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
//! Payload encoding posture (ADR-020 §4): payloads cross the trait as
//! [`serde_json::Value`]; engines serialize with serde_json and store
//! exactly those bytes — nothing re-encodes on the way out.
//!
//! Core provides the encode helper (the `Value` → stored-bytes
//! serialization the engines call) and the [`payload_as`] decode
//! convenience used by [`Job`](crate::Job) and
//! [`StreamEvent`](crate::StreamEvent). `serde`/`serde_json` are core's
//! only dependencies beyond `thiserror`.

use serde::de::DeserializeOwned;

use crate::Error;

/// Serialize a trait-crossing payload `Value` to the stored byte form.
///
/// The engines call this at enqueue/publish time and store exactly the
/// returned bytes (ADR-020 §4): the stored bytes of a job row or
/// stream event row are, contract-pinned, the serde_json serialization
/// of the `Value` the call carried. Cross-engine byte equality falls
/// out — the same `Value` serializes identically on both engines.
///
/// Fallible per ADR-023 §1: serialization failure is
/// [`Error::Codec`](crate::Error::Codec), typed at this helper — no
/// silent fallback form exists (an empty-byte store would decode to
/// `Null`, silent data corruption). Failures here are two-fold: a
/// `Value` nested deeper than the depth limit (`PAYLOAD_DEPTH_LIMIT`)
/// is rejected — the stored bytes could never be read back, and the
/// serializer's own unguarded recursion would otherwise abort deep
/// enough — and genuine serialization failure maps through the same
/// typed arm.
pub fn encode_payload(value: &serde_json::Value) -> Result<Vec<u8>, Error> {
    if value_depth(value) > PAYLOAD_DEPTH_LIMIT {
        return Err(Error::Codec(format!(
            "payload nesting exceeds the depth limit ({PAYLOAD_DEPTH_LIMIT})"
        )));
    }
    serde_json::to_vec(value).map_err(|e| Error::Codec(e.to_string()))
}

/// The payload nesting cap: aligned with serde_json's deserialization
/// recursion limit (a nested-open at container depth 128 fails to
/// decode; depths through 127 read back). A `Value` nested deeper than
/// the cap produces stored bytes no decoder can read — silent data
/// loss at read time — and deep enough the (unguarded) serializer
/// itself would overflow the stack abortably; the encode side refuses
/// it instead.
const PAYLOAD_DEPTH_LIMIT: usize = 127;

/// The maximum number of containers on any path through `value`
/// (iterative, not recursive — a recursive walk would itself overflow
/// the stack on exactly the deep inputs this guards against).
fn value_depth(value: &serde_json::Value) -> usize {
    let mut max = 0;
    let mut stack: Vec<(&serde_json::Value, usize)> = vec![(value, 1)];
    while let Some((current, depth)) = stack.pop() {
        match current {
            serde_json::Value::Array(items) => {
                if depth > max {
                    max = depth;
                }
                for item in items {
                    stack.push((item, depth + 1));
                }
            }
            serde_json::Value::Object(map) => {
                if depth > max {
                    max = depth;
                }
                for item in map.values() {
                    stack.push((item, depth + 1));
                }
            }
            _ => {}
        }
    }
    max
}

/// Decode stored payload bytes into a concrete target type.
///
/// Used by [`Job::payload_as`](crate::Job::payload_as) and
/// [`StreamEvent::payload_as`](crate::StreamEvent::payload_as). The
/// error is [`Error::Codec`] on any deserialization failure (pinned).
pub fn payload_as<T: DeserializeOwned>(payload: &[u8]) -> crate::Result<T> {
    serde_json::from_slice(payload).map_err(|e| Error::Codec(e.to_string()))
}