Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_pearlite/src/collab/op.rs

4.4 KiB, 23 runs

created by r1870400018:58602, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Mapping an annotation's lifecycle onto the Ore operation vocabulary.
2//!
3//! The operation log already carries a threaded-discussion vocabulary -- a proposal, replies to it,
4//! amendments of it, and a settlement of it -- and an annotation thread is exactly that shape. So no
5//! new operation kind is minted: an annotation is a proposal whose body is the annotation itself, and
6//! its later life is the operations that already speak about a proposal by name.
7//!
8//! - **create** -> [`Op::Proposal`], body the annotation.
9//! - **reply** -> [`Op::Said`] naming the opening proposal.
10//! - **edit** -> [`Op::Amended`] naming it, body the new annotation.
11//! - **resolve**/ **withdraw** -> [`Op::Settled`] naming it, `Done` or `Declined`.
12//!
13//! Each is wrapped in a record with a header (identity and parents) by its author and sealed by
14//! [`super::sign`]; this module supplies only the operation payloads and the decoding back out.
15
16use crate::collab::{
17 DocId,
18 DECODE_LIMITS,
19};
20
21use oxedyne_fe2o3_austenite::emit::pearl::Annotation;
22use oxedyne_fe2o3_ore::{
23 id::OpId,
24 op::{
25 Op,
26 Settled,
27 },
28};
29
30use oxedyne_fe2o3_core::prelude::*;
31use oxedyne_fe2o3_jdat::prelude::*;
32
33/// The forge voice annotation operations are written under, so the log can tell a Pearlite annotation
34/// thread from any other proposal it might come to carry.
35pub const VOICE: &str = "pearlite";
36
37/// Encodes an annotation as the body bytes an operation carries: its [`ToDat`] form in canonical
38/// binary daticle, the same encoding the store and the envelope speak, stamped with the document it
39/// belongs to so a fold can refuse it if it is replayed onto another document.
40fn annotation_body(doc: &DocId, ann: &Annotation) -> Outcome<Vec<u8>> {
41 let stamped = ann.clone().with_doc_id(doc.as_str());
42 Ok(res!(res!(stamped.to_dat()).to_bytes(Vec::new())))
43}
44
45/// Decodes an annotation from an operation body's bytes, the inverse of what [`create`] and [`edit`]
46/// store.
47///
48/// The bytes come from a peer over a relay, so they are decoded under the collaboration layer's bounds
49/// rather than trusted: a body that nests past [`DECODE_LIMITS`] is refused here rather than recursed
50/// into.
51pub fn annotation_from_body(body: &[u8]) -> Outcome<Annotation> {
52 let (dat, _) = res!(Dat::from_bytes_limited(body, &DECODE_LIMITS));
53 Annotation::from_dat(dat)
54}
55
56/// A new annotation opens a thread: an [`Op::Proposal`] whose body is the annotation and whose title
57/// is the block it hangs on, so a reader listing proposals sees what each is anchored to without
58/// decoding the body. The annotation body is stamped with `doc`, binding it to the one document.
59pub fn create(doc: &DocId, ann: &Annotation, time: u64) -> Outcome<Op> {
60 Ok(Op::Proposal {
61 title: ann.anchor.clone(),
62 body: res!(annotation_body(doc, ann)),
63 voice: VOICE.to_string(),
64 time,
65 })
66}
67
68/// A reply to an annotation thread: an [`Op::Said`] naming the opening proposal `on`.
69pub fn reply(on: OpId, text: &str, time: u64) -> Op {
70 Op::Said {
71 on,
72 text: text.as_bytes().to_vec(),
73 voice: VOICE.to_string(),
74 time,
75 }
76}
77
78/// An edit restates the annotation: an [`Op::Amended`] naming the opening proposal `on`, whose body is
79/// the new annotation, stamped with `doc` as [`create`] stamps it. The fold takes the latest amendment
80/// in time order, and only from the proposal's own signer.
81pub fn edit(doc: &DocId, on: OpId, ann: &Annotation, time: u64) -> Outcome<Op> {
82 Ok(Op::Amended {
83 on,
84 title: ann.anchor.clone(),
85 body: res!(annotation_body(doc, ann)),
86 voice: VOICE.to_string(),
87 time,
88 })
89}
90
91/// Resolving an annotation marks its thread done: an [`Op::Settled`] in the `Done` state. The
92/// annotation stays in a fold -- it is resolved, not gone.
93pub fn resolve(on: OpId, mark: Option<OpId>, time: u64) -> Op {
94 Op::Settled { on, state: Settled::Done, mark, time }
95}
96
97/// Withdrawing an annotation declines its thread: an [`Op::Settled`] in the `Declined` state, which a
98/// fold reads as a removal.
99pub fn withdraw(on: OpId, mark: Option<OpId>, time: u64) -> Op {
100 Op::Settled { on, state: Settled::Declined, mark, time }
101}
102
103/// The author's clock reading an operation carries, for the operations that carry one; the fold orders
104/// by it. An operation with no time of its own -- none of the annotation operations -- reads as zero.
105pub fn time_of(op: &Op) -> u64 {
106 match op {
107 Op::Proposal { time, .. } => *time,
108 Op::Said { time, .. } => *time,
109 Op::Settled { time, .. } => *time,
110 Op::Amended { time, .. } => *time,
111 _ => 0,
112 }
113}