Undoing a soft delete.
When she puts a memory away (a soft delete: it stays, only hidden) the store writes a [SoftAction] and the
shell offers "Undo" for [UNDO_WINDOW_MS]. undo(action_id) moves the thing back, and is refused after the
window, for an action that was already undone, and for anything that is not a soft action in the log: the
parents' permanent forget is never one, and forgetting a memory deletes its actions with it (the store's
schema cascades), so there is nothing left to undo and no way to write that down.
This module is the rule and nothing else: a store keeps the log and does the moving.
How long after a soft delete it can be undone: long enough to see the offer, read it or hear it and tap, short enough that the memory is not left in doubt.
13pub const UNDO_WINDOW_MS: u64 = 8_000;
What kind of soft delete an action was. One kind exists today; every new kind of soft delete is a variant here and a way to move it back in the store.
23impl SoftKind {
The word the store writes.
One entry of the log of soft deletes.
The identity of the memory it was done to.
46 pub gid: String,
When it was done, by the clock that will be asked about undoing it.
48 pub at_ms: u64,
There is no such soft action: the number was never one (a forget is not a soft action), or the memory it was done to has been forgotten.
58 Gone,
It was undone already.
60 Spent,
65impl std::fmt::Display for UndoRefusal { 66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 67 f.write_str(match self { 68 UndoRefusal::Gone => "there is nothing to undo", 69 UndoRefusal::Spent => "it was undone already", 70 UndoRefusal::Closed => "it is too late to undo it", 71 }) 72 } 73} 74 75impl std::error::Error for UndoRefusal {} 76 77impl SoftAction {
When the window closes.
How long is left of the window at now_ms; never more than the window, so a clock that went back does not
lengthen it.
Whether it may be undone at now_ms.
The newest action that can still be undone at now_ms: the one the shell offers.
106#[cfg(test)] 107mod tests { 108 use super::*; 109 110 fn action(id: u64, at_ms: u64) -> SoftAction { 111 SoftAction { id, kind: SoftKind::PutAway, gid: format!("g{id}"), at_ms, undone: false } 112 } 113 114 #[test] 115 fn the_window_is_eight_seconds_and_closes_exactly_there() { 116 assert_eq!(UNDO_WINDOW_MS, 8_000); 117 let a = action(1, 1_000); 118 assert_eq!(a.permits(1_000), Ok(())); 119 assert_eq!(a.permits(8_999), Ok(())); 120 assert_eq!(a.permits(9_000), Err(UndoRefusal::Closed)); 121 assert_eq!(a.permits(1_000_000), Err(UndoRefusal::Closed)); 122 } 123 124 #[test] 125 fn an_action_that_was_undone_cannot_be_undone_again_even_inside_the_window() { 126 let mut a = action(1, 1_000); 127 a.undone = true; 128 assert_eq!(a.permits(1_001), Err(UndoRefusal::Spent)); 129 } 130 131 #[test] 132 fn a_clock_that_went_back_does_not_lengthen_the_window() { 133 let a = action(1, 50_000); 134 assert_eq!(a.remaining_ms(10_000), UNDO_WINDOW_MS); 135 assert_eq!(a.remaining_ms(53_000), 5_000); 136 assert_eq!(a.remaining_ms(58_000), 0); 137 assert_eq!(a.remaining_ms(99_000), 0); 138 } 139 140 #[test] 141 fn the_offer_is_the_newest_action_still_open() { 142 let mut spent = action(3, 5_000); 143 spent.undone = true; 144 let mut actions = vec![action(1, 1_000), action(2, 4_000), spent]; 145 assert_eq!(newest_open(&actions, 6_000).map(|a| a.id), Some(2), "the newest is spent, so the one before it"); 146 assert_eq!(newest_open(&actions, 12_500).map(|a| a.id), None, "all closed or spent"); 147 actions.push(action(4, 90_000)); 148 assert_eq!(newest_open(&actions, 91_000).map(|a| a.id), Some(4)); 149 assert_eq!(newest_open(&[], 1), None); 150 } 151 152 #[test] 153 fn a_kind_is_written_and_read_by_its_word_only() { 154 assert_eq!(SoftKind::from_word(SoftKind::PutAway.word()), Some(SoftKind::PutAway)); 155 assert_eq!(SoftKind::from_word("forget"), None, "a forget is not a soft action"); 156 } 157}