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_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_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_STRING_BYTES {
135            return Err(Error::invalid_input(
136                "provenance source is empty or too large",
137            ));
138        }
139        if self.data.len() > MAX_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        let characters = self.short_name.chars().count();
167        if !(4..=50).contains(&characters) {
168            return Err(Error::invalid_input(
169                "short_name must contain between 4 and 50 characters",
170            ));
171        }
172        if self.short_description.chars().count() > 200 {
173            return Err(Error::invalid_input(
174                "short_description exceeds 200 characters",
175            ));
176        }
177        if self.long_description.split_whitespace().count() > 1_000 {
178            return Err(Error::invalid_input("long_description exceeds 1,000 words"));
179        }
180        if self.short_name.len() > MAX_STRING_BYTES
181            || self.short_description.len() > MAX_STRING_BYTES
182            || self.long_description.len() > MAX_STRING_BYTES
183        {
184            return Err(Error::invalid_input("node text exceeds 1 MiB"));
185        }
186        ensure_unique(&self.fixed_connections, "fixed_connections")?;
187        ensure_unique(&self.recent_connections, "recent_connections")?;
188        ensure_unique(&self.objects, "objects")?;
189        if self.fixed_connections.iter().any(|id| !id.valid_domain())
190            || self.recent_connections.iter().any(|id| !id.valid_domain())
191            || self.objects.iter().any(|id| !id.valid_domain())
192            || matches!(self.owner, Owner::Node(id) if !id.valid_domain())
193        {
194            return Err(Error::invalid_input(
195                "node contains an identifier in the wrong type domain",
196            ));
197        }
198        Ok(())
199    }
200}
201
202fn ensure_unique<T: Copy + Ord>(values: &[T], name: &str) -> Result<()> {
203    let mut ordered = values.to_vec();
204    ordered.sort_unstable();
205    if ordered.windows(2).any(|pair| pair[0] == pair[1]) {
206        return Err(Error::invalid_input(format!(
207            "{name} must contain unique IDs"
208        )));
209    }
210    Ok(())
211}
212
213#[derive(Clone, Debug, Eq, PartialEq)]
214pub struct Node {
215    pub id: NodeId,
216    pub data: NodeData,
217    pub last_author: String,
218    pub committed_at: DateTime<Utc>,
219}
220
221#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
222pub struct MergePair {
223    pub first: TransactionId,
224    pub second: TransactionId,
225}
226
227impl MergePair {
228    pub fn new(first: TransactionId, second: TransactionId) -> Result<Self> {
229        if first == second {
230            return Err(Error::invalid_input(
231                "a merge pair must name two different transactions",
232            ));
233        }
234        let (first, second) = if first < second {
235            (first, second)
236        } else {
237            (second, first)
238        };
239        Ok(Self { first, second })
240    }
241}
242
243#[derive(Clone, Debug, Eq, PartialEq)]
244pub struct HistoryEntry {
245    pub transaction_id: TransactionId,
246    pub writer: WriterId,
247    pub committed_at: DateTime<Utc>,
248    pub provenance: Provenance,
249    pub active: bool,
250    pub created: bool,
251    pub updated: bool,
252    pub data: Option<NodeData>,
253    pub merge_pairs: Vec<MergePair>,
254}
255
256#[derive(Clone, Debug, Eq, PartialEq)]
257pub struct NodeHistory {
258    pub node_id: NodeId,
259    pub frontier: Vec<TransactionId>,
260    pub visible: Option<TransactionId>,
261    pub entries: Vec<HistoryEntry>,
262}
263
264#[derive(Eq, PartialEq)]
265pub struct ObjectPayload {
266    pub id: ObjectId,
267    pub bytes: Vec<u8>,
268}
269
270impl fmt::Debug for ObjectPayload {
271    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
272        formatter
273            .debug_struct("ObjectPayload")
274            .field("id", &self.id)
275            .field("length", &self.bytes.len())
276            .finish()
277    }
278}
279
280#[derive(Eq, PartialEq)]
281pub struct TransactionPackage {
282    pub transaction: Vec<u8>,
283    pub objects: Vec<ObjectPayload>,
284}
285
286impl fmt::Debug for TransactionPackage {
287    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288        formatter
289            .debug_struct("TransactionPackage")
290            .field("transaction_bytes", &self.transaction.len())
291            .field("objects", &self.objects)
292            .finish()
293    }
294}