//! `DeadLetterService` โ€” Rhai SDK contract for replaying and resolving //! dead letters. Surface kept intentionally narrow for v1.1.1 (no //! `list` โ€” deferred to v1.2 per `docs/v1.1.x-design-notes.md` ยง4). //! //! Both methods are gated by `Capability::AppDeadLetterManage(AppId)` //! evaluated inside the impl. Public-HTTP scripts running with //! `cx.principal = None` will fail the check, which matches the //! design's expectation (managing dead letters is an admin act). use async_trait::async_trait; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; use crate::SdkCallCx; /// Opaque identifier for a `dead_letters` row. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct DeadLetterId(pub Uuid); impl DeadLetterId { #[must_use] pub fn new() -> Self { Self(Uuid::new_v4()) } #[must_use] pub fn into_inner(self) -> Uuid { self.0 } } impl Default for DeadLetterId { fn default() -> Self { Self::new() } } impl From for DeadLetterId { fn from(u: Uuid) -> Self { Self(u) } } impl From for Uuid { fn from(id: DeadLetterId) -> Self { id.0 } } impl std::fmt::Display for DeadLetterId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } #[async_trait] pub trait DeadLetterService: Send + Sync { /// Re-enqueue the original event into the outbox. The dead-letter /// row is marked `resolution = 'replayed'` regardless of whether /// the retry ultimately succeeds. async fn replay(&self, cx: &SdkCallCx, id: DeadLetterId) -> Result<(), DeadLetterError>; /// Mark the row resolved with the given reason (typically /// `"ignored"` from the dashboard or `"handled_by_script"` from /// inside a `dead_letter` trigger handler). async fn resolve( &self, cx: &SdkCallCx, id: DeadLetterId, reason: &str, ) -> Result<(), DeadLetterError>; } #[derive(Debug, Error)] pub enum DeadLetterError { #[error("dead-letter row not found")] NotFound, #[error("forbidden")] Forbidden, #[error("invalid resolution reason: {0}")] InvalidResolution(String), #[error("dead-letter backend error: {0}")] Backend(String), } /// Stub used to bootstrap the `Services` bundle before the real /// Postgres-backed implementation lands. Behaves like /// `NoopEventEmitter` โ€” every call returns `Backend("...")` so scripts /// see a clear "not yet implemented" error rather than silently /// no-op'ing. Replaced by `PostgresDeadLetterService` in the v1.1.1 /// dead-letter PR. #[derive(Debug, Default, Clone, Copy)] pub struct NoopDeadLetterService; #[async_trait] impl DeadLetterService for NoopDeadLetterService { async fn replay(&self, _cx: &SdkCallCx, _id: DeadLetterId) -> Result<(), DeadLetterError> { Err(DeadLetterError::Backend( "dead_letters::replay is not yet wired in".into(), )) } async fn resolve( &self, _cx: &SdkCallCx, _id: DeadLetterId, _reason: &str, ) -> Result<(), DeadLetterError> { Err(DeadLetterError::Backend( "dead_letters::resolve is not yet wired in".into(), )) } }