alkstore 0.1.0

The alkstore contract crate: one reactive store trait surface — notify, streams, durable queues, TTL locks, outbox, scheduler — implemented by the alkstore-sqlite/-postgres/-mem engine crates; no driver dependencies
Documentation
/// The crate's error taxonomy (ADR-008 §5): a small closed set of
/// contract-meaningful variants plus the opaque [`Error::Database`]
/// fallback. Nothing engine-specific ever rides the surface; driver
/// detail lives in the source chain of the opaque variant.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// The payload exceeds the engine's notification payload limit.
    ///
    /// Universal variant: the Postgres engine produces it (the
    /// client-side check on the serde_json serialization of the payload
    /// `Value` — the same byte string ADR-020 §4 pins as the stored row
    /// bytes); the SQLite engine never produces it (no limit). The
    /// documented workaround for large payloads on Postgres is the
    /// outbox shape — the id in the notification, the payload in a
    /// table row.
    #[error("payload exceeds the engine's notification payload limit (limit: {limit} bytes)")]
    PayloadTooLarge {
        /// The engine's byte limit that was exceeded.
        limit: usize,
    },
    /// A name carrying the reserved `__alkstore_` prefix was supplied
    /// to a shared-namespace entry point.
    #[error(
        "reserved name `{}`: names carrying the reserved `__alkstore_` prefix are engine-owned",
        display_safe(.name)
    )]
    ReservedName {
        /// The rejected name, as supplied.
        name: String,
    },
    /// A name failed entry-point validation.
    #[error("invalid name `{}`", display_safe(.name))]
    InvalidName {
        /// The rejected name, as supplied.
        name: String,
    },
    /// A schedule spec that doesn't parse under the v1 grammar
    /// (`@every <n><unit>` with `<n>` a positive integer and `<unit>`
    /// in `s|m|h|d`).
    ///
    /// Rejected by `schedule()` before storage, with no engine round
    /// trip; the caller fixes the spec string (ADR-009 §2, §6).
    #[error("invalid schedule spec `{}`", display_safe(.spec))]
    InvalidSpec {
        /// The spec string that failed the grammar.
        spec: String,
    },
    /// The `run_schedules` runner's leadership lock was lost mid-run.
    ///
    /// A distinct, matchable outcome from the clean `Ok(())` a stop
    /// token produces: the caller acts on it by respawning the runner
    /// (ADR-009 §1, §6).
    #[error("scheduler leadership lost")]
    LeadershipLost,
    /// An operation against a closed receiver/source.
    ///
    /// The closed arms of the wake receivers' non-`recv` forms
    /// (`try_recv`/`recv_timeout` — never
    /// `recv_timeout`'s timeout expiry, which yields `Ok(None)`) and
    /// the event receiver's `try_recv` on a `Closed`-caused stream
    /// receiver; never a timeout signal.
    #[error("the receiver or source is closed")]
    Closed,
    /// Payload serialization or deserialization failure.
    ///
    /// `payload_as` on stream events and job payloads; wakes carry no
    /// payload, so no wake path produces this variant.
    #[error("payload codec failure: {}", display_safe(.0))]
    Codec(String),
    /// The opaque fallback: everything else.
    ///
    /// Driver/connection/SQL errors, opaque to the contract; engine
    /// detail is preserved via the error source chain (`#[source]`). No
    /// engine-specific variants are ever minted for its contents.
    #[error("database error")]
    Database(#[source] Box<dyn std::error::Error + Send + Sync>),
}

/// Escape a consumer-supplied string for insertion into a rendered
/// error message.
///
/// Control characters (newlines, tabs, escape bytes) and backslash
/// quoting are routed through `char::escape_debug` so the rendered
/// message can never carry raw control bytes from consumer input —
/// the log-forging hygiene fix for the interpolating variants. The
/// stored field itself stays exactly as supplied; only the
/// `Display` text changes.
fn display_safe(s: &str) -> String {
    s.chars().flat_map(char::escape_debug).collect()
}

impl Error {
    /// Convenience constructor for the opaque [`Error::Database`]
    /// fallback.
    pub fn database<E>(err: E) -> Self
    where
        E: Into<Box<dyn std::error::Error + Send + Sync>>,
    {
        Error::Database(err.into())
    }
}

/// The crate's result type — every fallible operation in the contract
/// surface returns this. `Ok` never carries a "null" sentinel: no-work
/// claims and unlanded job/lock ops are values, not errors (ADR-008
/// §5).
pub type Result<T> = std::result::Result<T, Error>;