1use 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
15pub(crate) struct ResourceHistoryEdit<R> {
19 edit: HistoryEdit,
20 resource: std::marker::PhantomData<fn() -> R>,
21}
22
23impl<R> ResourceHistoryEdit<R> {
24 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#[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#[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
79pub(crate) fn part_action<R: tilo_resource::Resource>(part: &PartEdit) -> ResourceHistoryEdit<R> {
82 ResourceHistoryEdit::new(part.edit.clone())
83}
84
85pub(crate) fn recent(doc: &AutoCommit, last: usize) -> Vec<HistoryVersion> {
87 ui_core::history_source::verbs::recent(doc, last, &ThreadWords)
88}
89
90pub(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
98pub(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#[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 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 #[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], ®istry).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, ®istry).unwrap(),
231 tilo_actions::Verified::Reproduced
232 );
233 let before = peer.get_heads();
234 tilo_actions::replay(&mut peer, &transport.replicated[0], ®istry).unwrap();
235 assert_eq!(peer.get_heads(), before);
236 }
237}