Skip to main content

tilo_app/framework/
history.rs

1//! The resource's history as actions every framework app answers: the
2//! verbs themselves are [`ui_core::history_source::verbs`], the same ones a
3//! tilolang host answers for its instances. What is here says them in this
4//! thread's words ([`ThreadWords`]) and keeps the in-app history lists up
5//! to date.
6
7use ui_core::automerge::{AutoCommit, ChangeHash, Patch};
8use ui_core::history_source::ThreadWords;
9
10pub use ui_core::history_source::verbs::{
11    EDIT_TYPE, HistoryEdit, HistoryVersion, PART_EDIT_TYPE, PartEdit, VIEW_TYPE, is_edit,
12    part_edit_specs, review, specs, version_name,
13};
14
15/// A history edit of `R`'s document: `R`'s own registry is the only decoder
16/// for its recorded compensation. Wire context never installs a decoder or
17/// a caller.
18pub(crate) struct ResourceHistoryEdit<R> {
19    edit: HistoryEdit,
20    resource: std::marker::PhantomData<fn() -> R>,
21}
22
23impl<R> ResourceHistoryEdit<R> {
24    /// `edit` as `R`'s history applies it.
25    pub(crate) const fn new(edit: HistoryEdit) -> Self {
26        Self {
27            edit,
28            resource: std::marker::PhantomData,
29        }
30    }
31}
32
33impl<R> std::fmt::Debug for ResourceHistoryEdit<R> {
34    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        fmt_history_edit(&self.edit, formatter)
36    }
37}
38
39/// `edit` as `Debug` writes it, by one function for every resource.
40#[inline(never)]
41fn fmt_history_edit(
42    edit: &HistoryEdit,
43    formatter: &mut std::fmt::Formatter<'_>,
44) -> std::fmt::Result {
45    std::fmt::Debug::fmt(edit, formatter)
46}
47
48impl<'de, R> serde::Deserialize<'de> for ResourceHistoryEdit<R> {
49    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
50        let value = serde_json::Value::deserialize(deserializer)?;
51        Ok(Self::new(
52            history_edit(value).map_err(serde::de::Error::custom)?,
53        ))
54    }
55}
56
57/// A history edit from its JSON form, decoded by one function for every
58/// resource.
59#[inline(never)]
60fn history_edit(value: serde_json::Value) -> Result<HistoryEdit, serde_json::Error> {
61    <HistoryEdit as serde::Deserialize>::deserialize(value)
62}
63
64impl<R: tilo_resource::Resource> tilo_actions::Action for ResourceHistoryEdit<R> {
65    fn change_kind(&self) -> tilo_actions::ChangeKind {
66        self.edit.change_kind()
67    }
68
69    fn describe(&self, doc: &tilo_actions::Doc) -> Option<tilo_actions::Described> {
70        Some(self.edit.described(doc))
71    }
72
73    fn apply(&self, doc: &mut tilo_actions::Doc) -> Result<(), tilo_actions::ActionError> {
74        self.edit
75            .apply_registered(doc, tilo_resource::group::register::<R>, &ThreadWords)
76    }
77}
78
79/// The edit `part` names as the part's history applies it, decoding
80/// recorded actions with `R`'s registry.
81pub(crate) fn part_action<R: tilo_resource::Resource>(part: &PartEdit) -> ResourceHistoryEdit<R> {
82    ResourceHistoryEdit::new(part.edit.clone())
83}
84
85/// The last `last` versions, oldest first, in this thread's words.
86pub(crate) fn recent(doc: &AutoCommit, last: usize) -> Vec<HistoryVersion> {
87    ui_core::history_source::verbs::recent(doc, last, &ThreadWords)
88}
89
90/// Answer a history view in this thread's words.
91pub(crate) fn answer(
92    doc: &AutoCommit,
93    payload: &[u8],
94) -> Result<tilo_actions::ActionOutput, String> {
95    ui_core::history_source::verbs::answer(doc, payload, &ThreadWords)
96}
97
98/// What a history verb did, answered to its caller in this thread's words.
99pub(crate) fn result(
100    action: &str,
101    doc: &AutoCommit,
102    patches: &[Patch],
103) -> Option<serde_json::Value> {
104    ui_core::history_source::verbs::result(action, doc, patches, &ThreadWords)
105}
106
107/// The history lists views follow ([`ViewCx::history`](super::ViewCx::history)),
108/// brought up to date when the document moves.
109#[derive(Clone, Default)]
110pub(crate) struct HistoryFeeds(std::rc::Rc<std::cell::RefCell<FeedList>>);
111
112type Feed = (
113    std::rc::Weak<()>,
114    usize,
115    ui_core::LocalState<Vec<HistoryVersion>>,
116);
117
118#[derive(Default)]
119struct FeedList {
120    feeds: Vec<Feed>,
121    /// The heads the feeds last showed; empty forces a refresh.
122    heads: Vec<ChangeHash>,
123}
124
125impl HistoryFeeds {
126    pub(crate) fn follow(
127        &self,
128        alive: std::rc::Weak<()>,
129        last: usize,
130    ) -> ui_core::LocalState<Vec<HistoryVersion>> {
131        let state = ui_core::LocalState::new(Vec::new());
132        let mut registry = self.0.borrow_mut();
133        registry.feeds.push((alive, last, state.clone()));
134        registry.heads.clear();
135        state
136    }
137
138    /// Show the versions up to `doc`'s heads, when they moved.
139    #[inline(never)]
140    pub(crate) fn refresh(&self, doc: &AutoCommit) {
141        let mut list = self.0.borrow_mut();
142        list.feeds.retain(|(alive, _, _)| alive.strong_count() > 0);
143        if list.feeds.is_empty() {
144            return;
145        }
146        let heads = doc.committed_heads();
147        if list.heads == heads {
148            return;
149        }
150        let most = list
151            .feeds
152            .iter()
153            .map(|(_, last, _)| *last)
154            .max()
155            .unwrap_or(0);
156        let versions = recent(doc, most);
157        for (_, last, state) in &list.feeds {
158            let shown = versions
159                .get(versions.len().saturating_sub(*last)..)
160                .unwrap_or_default()
161                .to_vec();
162            if state.with(|current| *current != shown) {
163                state.set(shown);
164            }
165        }
166        list.heads = heads;
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use tilo_actions::EncodedAction;
174    use ui_core::automerge::ROOT;
175    use ui_core::automerge::transaction::Transactable;
176    use ui_core::history_source::verbs::review::RestoreReview;
177
178    #[derive(tilo_resource::Model, Default, Debug, Clone, PartialEq)]
179    pub struct ReplayBoard {
180        pub title: String,
181    }
182
183    #[tilo_resource::resource(kind = "test.review-replay")]
184    impl ReplayBoard {}
185
186    #[test]
187    fn reviewed_restore_replays_identically_beside_concurrent_work_and_duplicate_delivery() {
188        use ui_core::automerge::{ActorId, ReadDoc};
189        let mut origin = AutoCommit::new();
190        origin.put(ROOT, "title", "old").unwrap();
191        let target = origin.commit().unwrap();
192        origin.put(ROOT, "title", "new").unwrap();
193        origin.commit();
194        let response = review::answer(&origin, &target.to_string(), &["title".into()]).unwrap();
195        let review: RestoreReview = serde_json::from_value(response["review"].clone()).unwrap();
196        let edit = HistoryEdit::RestoreReviewed { review };
197        let encoded = EncodedAction {
198            type_id: EDIT_TYPE.into(),
199            payload: serde_json::to_vec(&edit).unwrap(),
200        };
201        let mut registry = tilo_actions::ActionRegistry::new();
202        registry.register::<ResourceHistoryEdit<ReplayBoard>>(EDIT_TYPE);
203        let action = registry.decode(&encoded).unwrap();
204        let mut peer = origin.fork();
205        peer.set_actor(ActorId::from(b"peer".as_slice()));
206        peer.put(ROOT, "unrelated", "keep").unwrap();
207        let unrelated = peer.commit().unwrap();
208        let mut transport = tilo_actions::LocalTransport::new();
209        tilo_actions::dispatch_with_context(
210            &mut origin,
211            action.as_ref(),
212            encoded,
213            &mut transport,
214            tilo_actions::ActionContext::for_resource("board", "test.review-replay", "Board"),
215        )
216        .unwrap();
217        let restored = origin.get_heads()[0];
218        tilo_actions::replay(&mut peer, &transport.replicated[0], &registry).unwrap();
219        assert!(peer.get_heads().contains(&restored));
220        assert!(peer.get_heads().contains(&unrelated));
221        assert_eq!(
222            peer.get(ROOT, "title").unwrap().unwrap().0.as_str(),
223            Some("old")
224        );
225        assert_eq!(
226            peer.get(ROOT, "unrelated").unwrap().unwrap().0.as_str(),
227            Some("keep")
228        );
229        assert_eq!(
230            tilo_actions::verify_change(&mut peer, restored, &registry).unwrap(),
231            tilo_actions::Verified::Reproduced
232        );
233        let before = peer.get_heads();
234        tilo_actions::replay(&mut peer, &transport.replicated[0], &registry).unwrap();
235        assert_eq!(peer.get_heads(), before);
236    }
237}