Skip to main content

blockworx/log/
change.rs

1//! The unit that is hashed, stored, sent and merged: a batch of commands with
2//! the heads its author had seen.
3
4use 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/// Milliseconds since the Unix epoch, for display only.
13///
14/// Never load-bearing: two replicas' wall clocks disagree, and a merge that
15/// consulted one would stop being reproducible. Ordering is [`Stamp`]'s job.
16#[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/// What the author was doing, for a history a person can read.
34///
35/// Free text rather than a tagged verb: the editor's real vocabulary is not
36/// settled yet, and a string cannot become wrong the way a half-guessed enum
37/// would. Narrowing it later means a [`ChangeV2`](Change), which is what the
38/// versioned envelope is for.
39#[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/// A node of the history DAG.
59///
60/// Versioned by variant rather than by a field. A `version: u16` only
61/// *describes* a shape; a variant *is* one — `ChangeV1`'s field list is frozen
62/// by construction, so its encoding, and therefore the hash every stored change
63/// is identified by, cannot drift. A shape change adds `V2` beside it and leaves
64/// every existing log meaning exactly what it meant.
65///
66/// It also gives the refuse-don't-guess rule for free: a `V2` written by a newer
67/// build fails to deserialize here rather than being reinterpreted as a `V1`.
68///
69/// Note what does *not* need a new version: adding a `PropWrite` variant. That
70/// is forward-incompatible (an older build refuses it) but backward-compatible
71/// (a newer build still reads old logs), which is the right asymmetry.
72#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
73pub enum Change {
74    V1(ChangeV1),
75}
76
77/// `parents` are the heads its author had when they made it, so a change with
78/// two or more parents *is* a merge.
79///
80/// **Frozen.** Every field here is part of a hash that identifies stored
81/// changes forever. Add a field and you have changed what every existing V1
82/// change hashes to — add `ChangeV2` instead.
83#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
84pub struct ChangeV1 {
85    /// The heads its author had seen. A set, because that is what it is: the
86    /// same heads always encode the same way and therefore hash the same,
87    /// without a constructor having to keep a `Vec` sorted and deduplicated.
88    pub parents: BTreeSet<ChangeHash>,
89    pub actor: ActorId,
90    pub lamport: Lamport,
91    pub wall_time: WallTime,
92    /// The lease scope this change was made under, when leases are in play.
93    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    /// Where the command at `index` sits in the global write order.
172    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    /// Content address over the canonical encoding, which includes the parent
180    /// hashes — so identical edits made against different histories are
181    /// different changes, and a hash commits to everything behind it.
182    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    /// Every command of this change, undone, in reverse order. Reversed so
191    /// that a batch which created an element and then set its properties
192    /// undoes the properties before the element goes away.
193    pub fn inverted_commands(&self) -> Vec<Command> {
194        self.commands()
195            .iter()
196            .rev()
197            .map(Command::inverted)
198            .collect()
199    }
200}
201
202/// Accumulates the commands of one gesture, then seals them into a change.
203///
204/// One builder makes one change and is consumed doing it: tools describe what
205/// they did, and the batch becomes a change at the gesture boundary. Sealing is
206/// the only place the clock is read, which is what keeps clocks out of the fold.
207#[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    /// `None` when nothing was recorded — a gesture that ended where it began
226    /// is not history, and must not burn a clock tick.
227    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    /// Heads are a set, so the order they are handed over in cannot reach the
265    /// encoding, and therefore cannot reach the hash.
266    #[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    /// Undoing a batch that built something up has to take it apart in the
318    /// opposite order.
319    #[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}