Skip to main content

kcode_kweb_db/
model.rs

1use crate::{NodeId, ObjectId, TransactionId, WriterId};
2use chrono::{DateTime, Utc};
3use std::{fmt, sync::Arc};
4
5pub const MAX_OBJECT_BYTES: u64 = 32 * 1024 * 1024 * 1024;
6pub const MAX_TRANSACTION_OBJECT_BYTES: u64 = MAX_OBJECT_BYTES;
7pub(crate) const MAX_PROVENANCE_STRING_BYTES: usize = 1024 * 1024;
8pub(crate) const MAX_TRANSACTION_BYTES: usize = 64 * 1024 * 1024;
9
10pub type Result<T> = std::result::Result<T, Error>;
11
12#[derive(Debug)]
13pub enum Error {
14    Io(std::io::Error),
15    Busy(String),
16    Corrupt(String),
17    InvalidConfig(String),
18    InvalidInput(String),
19    InvalidTransaction(String),
20    NotFound(String),
21    OfflineUpgradeRequired(String),
22}
23
24impl Error {
25    pub(crate) fn corrupt(message: impl Into<String>) -> Self {
26        Self::Corrupt(message.into())
27    }
28
29    pub(crate) fn invalid_config(message: impl Into<String>) -> Self {
30        Self::InvalidConfig(message.into())
31    }
32
33    pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
34        Self::InvalidInput(message.into())
35    }
36
37    pub(crate) fn invalid_transaction(message: impl Into<String>) -> Self {
38        Self::InvalidTransaction(message.into())
39    }
40
41    pub(crate) fn not_found(message: impl Into<String>) -> Self {
42        Self::NotFound(message.into())
43    }
44
45    pub(crate) fn offline_upgrade(message: impl Into<String>) -> Self {
46        Self::OfflineUpgradeRequired(message.into())
47    }
48}
49
50impl fmt::Display for Error {
51    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::Io(error) => write!(formatter, "I/O error: {error}"),
54            Self::Busy(message) => write!(formatter, "database is busy: {message}"),
55            Self::Corrupt(message) => write!(formatter, "database corruption: {message}"),
56            Self::InvalidConfig(message) => write!(formatter, "invalid configuration: {message}"),
57            Self::InvalidInput(message) => write!(formatter, "invalid input: {message}"),
58            Self::InvalidTransaction(message) => {
59                write!(formatter, "invalid transaction: {message}")
60            }
61            Self::NotFound(message) => write!(formatter, "not found: {message}"),
62            Self::OfflineUpgradeRequired(message) => {
63                write!(formatter, "offline upgrade required: {message}")
64            }
65        }
66    }
67}
68
69impl std::error::Error for Error {
70    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
71        match self {
72            Self::Io(error) => Some(error),
73            _ => None,
74        }
75    }
76}
77
78impl From<std::io::Error> for Error {
79    fn from(error: std::io::Error) -> Self {
80        Self::Io(error)
81    }
82}
83
84pub trait Gossip: Send + Sync + 'static {
85    fn announce(&self, package: TransactionPackage) -> bool;
86}
87
88pub trait TransactionSource: Send + Sync {
89    fn request_transactions(&self, transactions: Vec<TransactionId>);
90}
91
92#[derive(Clone, Copy, Debug, Default)]
93pub struct NoopGossip;
94
95impl Gossip for NoopGossip {
96    fn announce(&self, _package: TransactionPackage) -> bool {
97        true
98    }
99}
100
101#[derive(Clone)]
102pub struct Config {
103    pub signing_key: [u8; 32],
104    pub writers_by_priority: Vec<WriterId>,
105    pub gossip: Arc<dyn Gossip>,
106}
107
108impl fmt::Debug for Config {
109    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110        formatter
111            .debug_struct("Config")
112            .field("signing_key", &"[redacted]")
113            .field("writers_by_priority", &self.writers_by_priority)
114            .field("gossip", &"dyn Gossip")
115            .finish()
116    }
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct Provenance {
121    pub author: String,
122    pub source: String,
123    pub source_created_at: DateTime<Utc>,
124    pub data: String,
125}
126
127impl Provenance {
128    pub(crate) fn validate(&self) -> Result<()> {
129        if self.author.trim().is_empty() || self.author.len() > MAX_PROVENANCE_STRING_BYTES {
130            return Err(Error::invalid_input(
131                "provenance author is empty or too large",
132            ));
133        }
134        if self.source.trim().is_empty() || self.source.len() > MAX_PROVENANCE_STRING_BYTES {
135            return Err(Error::invalid_input(
136                "provenance source is empty or too large",
137            ));
138        }
139        if self.data.len() > MAX_PROVENANCE_STRING_BYTES {
140            return Err(Error::invalid_input("provenance data exceeds 1 MiB"));
141        }
142        Ok(())
143    }
144}
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147pub enum Owner {
148    Unowned,
149    SelfNode,
150    Node(NodeId),
151}
152
153#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct NodeData {
155    pub short_name: String,
156    pub short_description: String,
157    pub long_description: String,
158    pub owner: Owner,
159    pub fixed_connections: Vec<NodeId>,
160    pub recent_connections: Vec<NodeId>,
161    pub objects: Vec<ObjectId>,
162}
163
164impl NodeData {
165    pub(crate) fn validate(&self) -> Result<()> {
166        ensure_unique(&self.fixed_connections, "fixed_connections")?;
167        ensure_unique(&self.recent_connections, "recent_connections")?;
168        ensure_unique(&self.objects, "objects")?;
169        if self.fixed_connections.iter().any(|id| !id.valid_domain())
170            || self.recent_connections.iter().any(|id| !id.valid_domain())
171            || self.objects.iter().any(|id| !id.valid_domain())
172            || matches!(self.owner, Owner::Node(id) if !id.valid_domain())
173        {
174            return Err(Error::invalid_input(
175                "node contains an identifier in the wrong type domain",
176            ));
177        }
178        Ok(())
179    }
180}
181
182fn ensure_unique<T: Copy + Ord>(values: &[T], name: &str) -> Result<()> {
183    let mut ordered = values.to_vec();
184    ordered.sort_unstable();
185    if ordered.windows(2).any(|pair| pair[0] == pair[1]) {
186        return Err(Error::invalid_input(format!(
187            "{name} must contain unique IDs"
188        )));
189    }
190    Ok(())
191}
192
193#[derive(Clone, Debug, Eq, PartialEq)]
194pub struct Node {
195    pub id: NodeId,
196    pub data: NodeData,
197    pub last_author: String,
198    pub committed_at: DateTime<Utc>,
199}
200
201#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
202pub struct MergePair {
203    pub first: TransactionId,
204    pub second: TransactionId,
205}
206
207impl MergePair {
208    pub fn new(first: TransactionId, second: TransactionId) -> Result<Self> {
209        if first == second {
210            return Err(Error::invalid_input(
211                "a merge pair must name two different transactions",
212            ));
213        }
214        let (first, second) = if first < second {
215            (first, second)
216        } else {
217            (second, first)
218        };
219        Ok(Self { first, second })
220    }
221}
222
223#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct HistoryEntry {
225    pub transaction_id: TransactionId,
226    pub writer: WriterId,
227    pub committed_at: DateTime<Utc>,
228    pub provenance: Provenance,
229    pub active: bool,
230    pub created: bool,
231    pub updated: bool,
232    pub data: Option<NodeData>,
233    pub merge_pairs: Vec<MergePair>,
234}
235
236#[derive(Clone, Debug, Eq, PartialEq)]
237pub struct NodeHistory {
238    pub node_id: NodeId,
239    pub frontier: Vec<TransactionId>,
240    pub visible: Option<TransactionId>,
241    pub entries: Vec<HistoryEntry>,
242}
243
244#[derive(Eq, PartialEq)]
245pub struct ObjectPayload {
246    pub id: ObjectId,
247    pub bytes: Vec<u8>,
248}
249
250impl fmt::Debug for ObjectPayload {
251    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
252        formatter
253            .debug_struct("ObjectPayload")
254            .field("id", &self.id)
255            .field("length", &self.bytes.len())
256            .finish()
257    }
258}
259
260#[derive(Eq, PartialEq)]
261pub struct TransactionPackage {
262    pub transaction: Vec<u8>,
263    pub objects: Vec<ObjectPayload>,
264}
265
266impl fmt::Debug for TransactionPackage {
267    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
268        formatter
269            .debug_struct("TransactionPackage")
270            .field("transaction_bytes", &self.transaction.len())
271            .field("objects", &self.objects)
272            .finish()
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn node_text_has_no_database_policy_limits() {
282        let mut data = NodeData {
283            short_name: String::new(),
284            short_description: "x".repeat(201),
285            long_description: std::iter::repeat_n("word", 1_001)
286                .collect::<Vec<_>>()
287                .join(" "),
288            owner: Owner::SelfNode,
289            fixed_connections: Vec::new(),
290            recent_connections: Vec::new(),
291            objects: Vec::new(),
292        };
293
294        assert!(data.short_description.chars().count() > 200);
295        assert!(data.long_description.split_whitespace().count() > 1_000);
296        data.validate().unwrap();
297
298        data.short_name = "x".repeat(51);
299        assert!(data.short_name.chars().count() > 50);
300        data.validate().unwrap();
301
302        data.short_description = "x".repeat(MAX_PROVENANCE_STRING_BYTES + 1);
303        data.validate().unwrap();
304    }
305}