kcode-kweb-db 1.0.1

A disk-first convergent signed-DAG store for Kweb nodes and objects
Documentation
use crate::{NodeId, ObjectId, TransactionId, WriterId};
use chrono::{DateTime, Utc};
use std::{fmt, sync::Arc};

pub const MAX_OBJECT_BYTES: u64 = 32 * 1024 * 1024 * 1024;
pub const MAX_TRANSACTION_OBJECT_BYTES: u64 = MAX_OBJECT_BYTES;
pub(crate) const MAX_STRING_BYTES: usize = 1024 * 1024;
pub(crate) const MAX_TRANSACTION_BYTES: usize = 64 * 1024 * 1024;

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug)]
pub enum Error {
    Io(std::io::Error),
    Busy(String),
    Corrupt(String),
    InvalidConfig(String),
    InvalidInput(String),
    InvalidTransaction(String),
    NotFound(String),
    OfflineUpgradeRequired(String),
}

impl Error {
    pub(crate) fn corrupt(message: impl Into<String>) -> Self {
        Self::Corrupt(message.into())
    }

    pub(crate) fn invalid_config(message: impl Into<String>) -> Self {
        Self::InvalidConfig(message.into())
    }

    pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
        Self::InvalidInput(message.into())
    }

    pub(crate) fn invalid_transaction(message: impl Into<String>) -> Self {
        Self::InvalidTransaction(message.into())
    }

    pub(crate) fn not_found(message: impl Into<String>) -> Self {
        Self::NotFound(message.into())
    }

    pub(crate) fn offline_upgrade(message: impl Into<String>) -> Self {
        Self::OfflineUpgradeRequired(message.into())
    }
}

impl fmt::Display for Error {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io(error) => write!(formatter, "I/O error: {error}"),
            Self::Busy(message) => write!(formatter, "database is busy: {message}"),
            Self::Corrupt(message) => write!(formatter, "database corruption: {message}"),
            Self::InvalidConfig(message) => write!(formatter, "invalid configuration: {message}"),
            Self::InvalidInput(message) => write!(formatter, "invalid input: {message}"),
            Self::InvalidTransaction(message) => {
                write!(formatter, "invalid transaction: {message}")
            }
            Self::NotFound(message) => write!(formatter, "not found: {message}"),
            Self::OfflineUpgradeRequired(message) => {
                write!(formatter, "offline upgrade required: {message}")
            }
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io(error) => Some(error),
            _ => None,
        }
    }
}

impl From<std::io::Error> for Error {
    fn from(error: std::io::Error) -> Self {
        Self::Io(error)
    }
}

pub trait Gossip: Send + Sync + 'static {
    fn announce(&self, package: TransactionPackage) -> bool;
}

pub trait TransactionSource: Send + Sync {
    fn request_transactions(&self, transactions: Vec<TransactionId>);
}

#[derive(Clone, Copy, Debug, Default)]
pub struct NoopGossip;

impl Gossip for NoopGossip {
    fn announce(&self, _package: TransactionPackage) -> bool {
        true
    }
}

#[derive(Clone)]
pub struct Config {
    pub signing_key: [u8; 32],
    pub writers_by_priority: Vec<WriterId>,
    pub gossip: Arc<dyn Gossip>,
}

impl fmt::Debug for Config {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("Config")
            .field("signing_key", &"[redacted]")
            .field("writers_by_priority", &self.writers_by_priority)
            .field("gossip", &"dyn Gossip")
            .finish()
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Provenance {
    pub author: String,
    pub source: String,
    pub source_created_at: DateTime<Utc>,
    pub data: String,
}

impl Provenance {
    pub(crate) fn validate(&self) -> Result<()> {
        if self.author.trim().is_empty() || self.author.len() > MAX_STRING_BYTES {
            return Err(Error::invalid_input(
                "provenance author is empty or too large",
            ));
        }
        if self.source.trim().is_empty() || self.source.len() > MAX_STRING_BYTES {
            return Err(Error::invalid_input(
                "provenance source is empty or too large",
            ));
        }
        if self.data.len() > MAX_STRING_BYTES {
            return Err(Error::invalid_input("provenance data exceeds 1 MiB"));
        }
        Ok(())
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Owner {
    Unowned,
    SelfNode,
    Node(NodeId),
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NodeData {
    pub short_name: String,
    pub short_description: String,
    pub long_description: String,
    pub owner: Owner,
    pub fixed_connections: Vec<NodeId>,
    pub recent_connections: Vec<NodeId>,
    pub objects: Vec<ObjectId>,
}

impl NodeData {
    pub(crate) fn validate(&self) -> Result<()> {
        let characters = self.short_name.chars().count();
        if !(4..=50).contains(&characters) {
            return Err(Error::invalid_input(
                "short_name must contain between 4 and 50 characters",
            ));
        }
        if self.short_description.chars().count() > 200 {
            return Err(Error::invalid_input(
                "short_description exceeds 200 characters",
            ));
        }
        if self.long_description.split_whitespace().count() > 1_000 {
            return Err(Error::invalid_input("long_description exceeds 1,000 words"));
        }
        if self.short_name.len() > MAX_STRING_BYTES
            || self.short_description.len() > MAX_STRING_BYTES
            || self.long_description.len() > MAX_STRING_BYTES
        {
            return Err(Error::invalid_input("node text exceeds 1 MiB"));
        }
        ensure_unique(&self.fixed_connections, "fixed_connections")?;
        ensure_unique(&self.recent_connections, "recent_connections")?;
        ensure_unique(&self.objects, "objects")?;
        if self.fixed_connections.iter().any(|id| !id.valid_domain())
            || self.recent_connections.iter().any(|id| !id.valid_domain())
            || self.objects.iter().any(|id| !id.valid_domain())
            || matches!(self.owner, Owner::Node(id) if !id.valid_domain())
        {
            return Err(Error::invalid_input(
                "node contains an identifier in the wrong type domain",
            ));
        }
        Ok(())
    }
}

fn ensure_unique<T: Copy + Ord>(values: &[T], name: &str) -> Result<()> {
    let mut ordered = values.to_vec();
    ordered.sort_unstable();
    if ordered.windows(2).any(|pair| pair[0] == pair[1]) {
        return Err(Error::invalid_input(format!(
            "{name} must contain unique IDs"
        )));
    }
    Ok(())
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Node {
    pub id: NodeId,
    pub data: NodeData,
    pub last_author: String,
    pub committed_at: DateTime<Utc>,
}

#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct MergePair {
    pub first: TransactionId,
    pub second: TransactionId,
}

impl MergePair {
    pub fn new(first: TransactionId, second: TransactionId) -> Result<Self> {
        if first == second {
            return Err(Error::invalid_input(
                "a merge pair must name two different transactions",
            ));
        }
        let (first, second) = if first < second {
            (first, second)
        } else {
            (second, first)
        };
        Ok(Self { first, second })
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HistoryEntry {
    pub transaction_id: TransactionId,
    pub writer: WriterId,
    pub committed_at: DateTime<Utc>,
    pub provenance: Provenance,
    pub active: bool,
    pub created: bool,
    pub updated: bool,
    pub data: Option<NodeData>,
    pub merge_pairs: Vec<MergePair>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NodeHistory {
    pub node_id: NodeId,
    pub frontier: Vec<TransactionId>,
    pub visible: Option<TransactionId>,
    pub entries: Vec<HistoryEntry>,
}

#[derive(Eq, PartialEq)]
pub struct ObjectPayload {
    pub id: ObjectId,
    pub bytes: Vec<u8>,
}

impl fmt::Debug for ObjectPayload {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("ObjectPayload")
            .field("id", &self.id)
            .field("length", &self.bytes.len())
            .finish()
    }
}

#[derive(Eq, PartialEq)]
pub struct TransactionPackage {
    pub transaction: Vec<u8>,
    pub objects: Vec<ObjectPayload>,
}

impl fmt::Debug for TransactionPackage {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("TransactionPackage")
            .field("transaction_bytes", &self.transaction.len())
            .field("objects", &self.objects)
            .finish()
    }
}