1use std::collections::BTreeSet;
5
6use serde::{Deserialize, Serialize};
7
8use crate::log::encode;
9use crate::log::id::{ActorId, BatchIndex, ChangeHash, Clock, Lamport, Stamp, WriteOrder};
10use crate::log::{Command, ElementId};
11
12#[derive(
17 Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize,
18)]
19pub struct WallTime(i64);
20
21impl WallTime {
22 pub const UNKNOWN: WallTime = WallTime(0);
23
24 pub const fn from_millis(millis: i64) -> Self {
25 Self(millis)
26 }
27
28 pub const fn millis(self) -> i64 {
29 self.0
30 }
31}
32
33#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Serialize, Deserialize)]
40pub struct SemanticLabel(String);
41
42impl SemanticLabel {
43 pub fn new(text: impl Into<String>) -> Self {
44 Self(text.into())
45 }
46
47 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52impl std::fmt::Display for SemanticLabel {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.write_str(&self.0)
55 }
56}
57
58#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
73pub enum Change {
74 V1(ChangeV1),
75}
76
77#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
84pub struct ChangeV1 {
85 pub parents: BTreeSet<ChangeHash>,
89 pub actor: ActorId,
90 pub lamport: Lamport,
91 pub wall_time: WallTime,
92 pub scope: Option<ElementId>,
94 pub label: SemanticLabel,
95 pub commands: Vec<Command>,
96}
97
98impl Change {
99 pub fn new(
100 stamp: Stamp,
101 parents: impl IntoIterator<Item = ChangeHash>,
102 label: SemanticLabel,
103 commands: Vec<Command>,
104 ) -> Self {
105 Self::V1(ChangeV1 {
106 parents: parents.into_iter().collect(),
107 actor: stamp.actor,
108 lamport: stamp.lamport,
109 wall_time: WallTime::UNKNOWN,
110 scope: None,
111 label,
112 commands,
113 })
114 }
115
116 fn v1(&self) -> &ChangeV1 {
117 match self {
118 Self::V1(change) => change,
119 }
120 }
121
122 fn v1_mut(&mut self) -> &mut ChangeV1 {
123 match self {
124 Self::V1(change) => change,
125 }
126 }
127
128 pub fn parents(&self) -> &BTreeSet<ChangeHash> {
129 &self.v1().parents
130 }
131
132 pub fn actor(&self) -> ActorId {
133 self.v1().actor
134 }
135
136 pub fn lamport(&self) -> Lamport {
137 self.v1().lamport
138 }
139
140 pub fn label(&self) -> &SemanticLabel {
141 &self.v1().label
142 }
143
144 pub fn commands(&self) -> &[Command] {
145 &self.v1().commands
146 }
147
148 pub fn wall_time(&self) -> WallTime {
149 self.v1().wall_time
150 }
151
152 pub fn set_wall_time(&mut self, at: WallTime) {
153 self.v1_mut().wall_time = at;
154 }
155
156 pub fn scope(&self) -> Option<ElementId> {
157 self.v1().scope
158 }
159
160 pub fn set_scope(&mut self, scope: Option<ElementId>) {
161 self.v1_mut().scope = scope;
162 }
163
164 pub fn stamp(&self) -> Stamp {
165 Stamp {
166 lamport: self.lamport(),
167 actor: self.actor(),
168 }
169 }
170
171 pub fn order_of(&self, index: usize) -> WriteOrder {
173 WriteOrder::new(
174 self.stamp(),
175 BatchIndex::new(u32::try_from(index).unwrap_or(u32::MAX)),
176 )
177 }
178
179 pub fn hash(&self) -> ChangeHash {
183 ChangeHash::from_bytes(*blake3::hash(&encode::to_bytes(self)).as_bytes())
184 }
185
186 pub fn is_merge(&self) -> bool {
187 self.parents().len() > 1
188 }
189
190 pub fn inverted_commands(&self) -> Vec<Command> {
194 self.commands()
195 .iter()
196 .rev()
197 .map(Command::inverted)
198 .collect()
199 }
200}
201
202#[derive(Debug, Default)]
208pub struct ChangeBuilder {
209 commands: Vec<Command>,
210}
211
212impl ChangeBuilder {
213 pub fn new() -> Self {
214 Self::default()
215 }
216
217 pub fn push(&mut self, command: Command) {
218 self.commands.push(command);
219 }
220
221 pub fn is_empty(&self) -> bool {
222 self.commands.is_empty()
223 }
224
225 pub fn seal(
228 self,
229 clock: &mut Clock,
230 heads: impl IntoIterator<Item = ChangeHash>,
231 label: SemanticLabel,
232 ) -> Option<Change> {
233 if self.commands.is_empty() {
234 return None;
235 }
236 Some(Change::new(clock.tick(), heads, label, self.commands))
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::log::command::{ElementKind, PropSet};
244
245 fn hash(byte: u8) -> ChangeHash {
246 ChangeHash::from_bytes([byte; 32])
247 }
248
249 fn a_change(parents: impl IntoIterator<Item = ChangeHash>) -> Change {
250 let mut clock = Clock::new(ActorId::from_uuid(uuid::Uuid::from_bytes([7; 16])));
251 Change::new(
252 clock.tick(),
253 parents,
254 SemanticLabel::new("test"),
255 vec![Command::Create {
256 id: ElementId::from_uuid(uuid::Uuid::from_bytes([3; 16])),
257 kind: ElementKind::Block,
258 parent: ElementId::DOCUMENT,
259 init: PropSet::new(),
260 }],
261 )
262 }
263
264 #[test]
267 fn parent_order_does_not_change_the_hash() {
268 let forwards = a_change([hash(1), hash(2)]);
269 let backwards = a_change([hash(2), hash(1)]);
270 assert_eq!(forwards.parents(), backwards.parents());
271 assert_eq!(forwards.hash(), backwards.hash());
272 }
273
274 #[test]
275 fn a_repeated_parent_is_recorded_once() {
276 let change = a_change([hash(1), hash(1)]);
277 assert_eq!(change.parents(), &BTreeSet::from([hash(1)]));
278 assert!(!change.is_merge());
279 }
280
281 #[test]
282 fn parents_are_part_of_the_hash() {
283 assert_ne!(a_change([hash(1)]).hash(), a_change([hash(2)]).hash());
284 }
285
286 #[test]
287 fn an_empty_builder_seals_to_nothing() {
288 let mut clock = Clock::new(ActorId::new());
289 let builder = ChangeBuilder::new();
290 assert!(
291 builder
292 .seal(&mut clock, [], SemanticLabel::new("noop"))
293 .is_none()
294 );
295 assert_eq!(
296 clock.lamport(),
297 Lamport::ZERO,
298 "an empty seal must not burn a lamport tick"
299 );
300 }
301
302 #[test]
303 fn sealing_carries_the_pushed_commands_into_the_change() {
304 let mut clock = Clock::new(ActorId::new());
305 let id = ElementId::new();
306 let mut builder = ChangeBuilder::new();
307 builder.push(Command::Delete { id });
308 assert!(!builder.is_empty());
309
310 let sealed = builder
311 .seal(&mut clock, [], SemanticLabel::new("delete"))
312 .expect("a command was recorded");
313 assert_eq!(sealed.commands().to_vec(), vec![Command::Delete { id }]);
314 assert_eq!(sealed.lamport(), Lamport::new(1));
315 }
316
317 #[test]
320 fn inverted_commands_run_backwards() {
321 let id = ElementId::new();
322 let change = Change::new(
323 Stamp {
324 lamport: Lamport::new(1),
325 actor: ActorId::new(),
326 },
327 [],
328 SemanticLabel::new("create then delete"),
329 vec![
330 Command::Create {
331 id,
332 kind: ElementKind::Block,
333 parent: ElementId::DOCUMENT,
334 init: PropSet::new(),
335 },
336 Command::Delete { id },
337 ],
338 );
339
340 assert_eq!(
341 change.inverted_commands(),
342 vec![Command::Restore { id }, Command::Delete { id }]
343 );
344 }
345}