haematite 0.6.1

Content-addressed, branchable, actor-native storage engine
Documentation
//! Reload-scoped browser-local durable state, available only on wasm32.

mod codec;
mod commit;
mod core;
mod js;
mod node;
mod opfs;
mod opfs_io;
mod owner;
mod source;

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

pub use core::{BrowserLocalStore, BrowserWorkHandle};

/// The only commit mode admitted by this façade.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserCommitDurability {
    Durable,
}

/// The only durability grade this reload-scoped façade can acknowledge.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserDurabilityGrade {
    BrowserFlushed,
}

impl BrowserDurabilityGrade {
    #[must_use]
    pub const fn label(self) -> &'static str {
        match self {
            Self::BrowserFlushed => "D-browser-flushed",
        }
    }
}

/// Durable branch identity after a browser commit.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrowserBranchVersion {
    pub name: String,
    pub created: u64,
    pub seq: u64,
}

/// Receipt returned only after nodes, record, pins, and handle heads agree.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrowserCommitReceipt {
    pub grade: BrowserDurabilityGrade,
    pub version: BrowserBranchVersion,
    pub heads: Vec<(crate::ShardId, crate::tree::Hash)>,
    pub advanced: bool,
}

/// Awaitable browser request. Completion is driven by browser promises/events.
pub struct BrowserRequest<T> {
    future: Pin<Box<dyn Future<Output = Result<T, BrowserLocalError>>>>,
}

impl<T> BrowserRequest<T> {
    pub(crate) fn new(
        future: impl Future<Output = Result<T, BrowserLocalError>> + 'static,
    ) -> Self {
        Self {
            future: Box::pin(future),
        }
    }
}

impl<T> Future for BrowserRequest<T> {
    type Output = Result<T, BrowserLocalError>;

    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
        self.future.as_mut().poll(context)
    }
}

/// Closed typed sources retained by browser-local backend refusals.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BrowserSourceError {
    JavaScript {
        operation: &'static str,
        name: String,
        message: String,
    },
    Codec {
        message: String,
    },
    Node {
        error: crate::tree::NodeError,
    },
    NodeHashMismatch {
        expected: crate::tree::Hash,
        actual: crate::tree::Hash,
    },
    Tree {
        error: crate::tree::TreeError,
    },
    Branch {
        error: crate::branch::BranchError,
    },
    Backend {
        operation: &'static str,
        message: String,
    },
}

impl BrowserSourceError {
    pub(crate) fn codec(message: impl Into<String>) -> Self {
        Self::Codec {
            message: message.into(),
        }
    }

    pub(crate) fn backend(operation: &'static str, message: impl Into<String>) -> Self {
        Self::Backend {
            operation,
            message: message.into(),
        }
    }
}

impl std::fmt::Display for BrowserSourceError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::JavaScript {
                operation,
                name,
                message,
            } if name.is_empty() => write!(formatter, "{operation}: {message}"),
            Self::JavaScript {
                operation,
                name,
                message,
            } => write!(formatter, "{operation}: {name}: {message}"),
            Self::Codec { message } => formatter.write_str(message),
            Self::Node { error } => error.fmt(formatter),
            Self::NodeHashMismatch { expected, actual } => write!(
                formatter,
                "node hash does not match its bytes: expected {expected}, actual {actual}"
            ),
            Self::Tree { error } => error.fmt(formatter),
            Self::Branch { error } => error.fmt(formatter),
            Self::Backend { operation, message } => {
                write!(formatter, "{operation}: {message}")
            }
        }
    }
}

impl From<String> for BrowserSourceError {
    fn from(message: String) -> Self {
        Self::codec(message)
    }
}

impl From<&str> for BrowserSourceError {
    fn from(message: &str) -> Self {
        Self::codec(message)
    }
}

impl std::error::Error for BrowserSourceError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Node { error } => Some(error),
            Self::Tree { error } => Some(error),
            Self::Branch { error } => Some(error),
            Self::JavaScript { .. }
            | Self::Codec { .. }
            | Self::NodeHashMismatch { .. }
            | Self::Backend { .. } => None,
        }
    }
}

/// Closed refusal family for the minimal browser-local slice.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BrowserLocalError {
    UnsupportedSubstrate {
        probe: &'static str,
        browser: String,
        evidence: &'static str,
    },
    UnsupportedStoreFormat {
        found: String,
    },
    StoreGenerationLost {
        component: &'static str,
    },
    NonCanonicalBranchSet {
        records: usize,
    },
    WrongBranchKind {
        found: String,
    },
    NodeFailure {
        operation: &'static str,
        source: BrowserSourceError,
    },
    RecordFailure {
        operation: &'static str,
        source: BrowserSourceError,
    },
    FenceFailure {
        operation: &'static str,
        source: BrowserSourceError,
    },
    WorkerClosed,
}

impl std::fmt::Display for BrowserLocalError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::UnsupportedSubstrate { probe, browser, .. } => {
                write!(
                    formatter,
                    "unsupported browser substrate for {probe}: {browser}"
                )
            }
            Self::UnsupportedStoreFormat { found } => {
                write!(formatter, "unsupported browser store format: {found}")
            }
            Self::StoreGenerationLost { component } => {
                write!(formatter, "browser store generation lost: {component}")
            }
            Self::NonCanonicalBranchSet { records } => {
                write!(
                    formatter,
                    "non-canonical browser branch set: {records} records"
                )
            }
            Self::WrongBranchKind { found } => write!(formatter, "wrong branch kind: {found}"),
            Self::NodeFailure { operation, source }
            | Self::RecordFailure { operation, source }
            | Self::FenceFailure { operation, source } => {
                write!(formatter, "browser {operation} failed: {source}")
            }
            Self::WorkerClosed => formatter.write_str("browser-local owner worker is closed"),
        }
    }
}

impl std::error::Error for BrowserLocalError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::NodeFailure { source, .. }
            | Self::RecordFailure { source, .. }
            | Self::FenceFailure { source, .. } => Some(source),
            Self::UnsupportedSubstrate { .. }
            | Self::UnsupportedStoreFormat { .. }
            | Self::StoreGenerationLost { .. }
            | Self::NonCanonicalBranchSet { .. }
            | Self::WrongBranchKind { .. }
            | Self::WorkerClosed => None,
        }
    }
}