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
//! Queue value types (ADR-019 §3, ADR-021 §2): `Job` and `JobState`.
//!
//! `#[non_exhaustive]` per ADR-017 §3 — consumers read but never
//! construct; field additions ride ADR-017 class 2 (semver-minor).

use serde::de::DeserializeOwned;

use crate::payload;

/// A job's lifecycle state. Dead-letter is a move to dead storage
/// (ADR-010 §4); `get_job` sees dead rows with `last_error`/`died_at`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum JobState {
    /// Enqueued, awaiting its ready time or a claim.
    Pending,
    /// Currently claimed and running (or within its claim deadline).
    Processing,
    /// Dead-lettered.
    Dead,
}

/// A job row, as returned by `get_job` and carried inside a claim's
/// boxed `JobHandle` (`job(&self) -> &Job`).
///
/// `#[non_exhaustive]` (ADR-017 §3). The resolved values — not the raw
/// enqueue fields — are what a `Job` shows: the ready time after
/// delay-over-`run_at` resolution, the absolute `expires_at` after
/// relative-`expires` resolution (ADR-020 §1–§2), and the `QueueOpts`
/// stamps attached at enqueue (ADR-010 §3a).
///
/// Dead rows carry their stamps too (`get_job` returns the stamps with
/// the row); `last_error`/`died_at` are dead-only (`None` on live
/// rows).
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Job {
    /// The row id.
    pub id: i64,
    /// The queue name the job was enqueued into.
    pub queue: String,
    /// The lifecycle state.
    pub state: JobState,
    /// Payload bytes — the serde_json serialization of the trait
    /// `Value` at enqueue time (ADR-020 §4); decode with
    /// [`Job::payload_as`].
    pub payload: Vec<u8>,
    /// The claim-order key (priority DESC).
    pub priority: i64,
    /// The resolved ready time — the claim-order key within equal
    /// priority (unix seconds).
    pub run_at: i64,
    /// Claim count; every claim counts, reclaims included.
    pub attempts: i64,
    /// The row's max-attempts stamp (resolved at enqueue).
    pub max_attempts: i64,
    /// The claimant (`worker_id` on claims); `None` pre-claim.
    pub worker_id: Option<String>,
    /// Unix seconds at the most recent claim (fresh or reclaim);
    /// `None` before any claim (ADR-021 §2) — the dual-execution
    /// window's diagnostic field, paired with `claim_expires_at`.
    pub claimed_at: Option<i64>,
    /// The claim deadline (unix seconds); `None` pre-claim.
    pub claim_expires_at: Option<i64>,
    /// Unix seconds at enqueue.
    pub created_at: i64,
    /// The resolved absolute job-level expiry; `None` = never expires.
    pub expires_at: Option<i64>,
    /// The visibility-timeout stamp resolved at enqueue.
    pub visibility_timeout_s: i64,
    /// The backoff-base stamp resolved at enqueue.
    pub backoff_base_s: i64,
    /// The dead-letter-retention stamp resolved at enqueue; `None` =
    /// dead rows live forever.
    pub dead_letter_retention_s: Option<i64>,
    /// Dead-only: the recorded failure reason (`None` on live rows).
    pub last_error: Option<String>,
    /// Dead-only: unix seconds at death (`None` on live rows).
    pub died_at: Option<i64>,
}

impl Job {
    /// Engine-construction-only constructor taking the full field set.
    ///
    /// # Contract posture
    ///
    /// Not consumer API and not part of the contract surface: engines
    /// (in-repo, lockstep-updated with core) use it to build [`Job`]
    /// values from row data, since `#[non_exhaustive]` (ADR-017 §3)
    /// bans struct-literal construction downstream. Not covered by
    /// ADR-017's semver-minor field-addition promise — a `Job` field
    /// addition changes this signature, which is an engine breaking
    /// change under a core change and a lockstep-duty event
    /// (ADR-017 §5). Hidden from consumer-facing docs.
    #[doc(hidden)]
    #[allow(clippy::too_many_arguments)]
    pub fn from_row(
        id: i64,
        queue: String,
        state: JobState,
        payload: Vec<u8>,
        priority: i64,
        run_at: i64,
        attempts: i64,
        max_attempts: i64,
        worker_id: Option<String>,
        claimed_at: Option<i64>,
        claim_expires_at: Option<i64>,
        created_at: i64,
        expires_at: Option<i64>,
        visibility_timeout_s: i64,
        backoff_base_s: i64,
        dead_letter_retention_s: Option<i64>,
        last_error: Option<String>,
        died_at: Option<i64>,
    ) -> Self {
        Self {
            id,
            queue,
            state,
            payload,
            priority,
            run_at,
            attempts,
            max_attempts,
            worker_id,
            claimed_at,
            claim_expires_at,
            created_at,
            expires_at,
            visibility_timeout_s,
            backoff_base_s,
            dead_letter_retention_s,
            last_error,
            died_at,
        }
    }

    /// Decode the stored payload bytes into a concrete target type.
    ///
    /// The stored bytes are the serde_json serialization of the
    /// enqueue-time `Value` (ADR-020 §4); any deserialization failure
    /// is [`Error::Codec`](crate::Error::Codec).
    pub fn payload_as<T: DeserializeOwned>(&self) -> crate::Result<T> {
        payload::payload_as(&self.payload)
    }
}